-
Notifications
You must be signed in to change notification settings - Fork 208
PHPC-790: Accept integer and floats in Timestamp and UTCDateTime constructors #467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
bson_ascii_strtoll() will support range checking in libbson 1.5.0 (CDRIVER-1377), so there is no longer any benefit of using _atoi64() on Windows. This will make parsing consistent across both platforms, since _atoi64() does not report errors for non-integer strings (e.g. "1.2").
d972e3c
to
23ca7ed
Compare
@@ -91,22 +91,14 @@ static bool php_phongo_timestamp_init_from_string(php_phongo_timestamp_t *intern | |||
* available on all platforms (e.g. HP-UX), and atoll() provides no error | |||
* reporting at all. */ | |||
|
|||
#if defined(PHP_WIN32) | |||
increment = _atoi64(s_increment); | |||
#else | |||
increment = bson_ascii_strtoll(s_increment, &endptr, 10); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_atoi64()
would parse "1234.5" as 1234 and raise no error, while bson_ascii_strtoll()
is more strict. Also, bson_ascii_strtoll()
now supports out-of-range error reporting in libbson 1.5.0 (CDRIVER-1337). That was the only benefit of using _atoi64()
for Windows before, so I see no reason to use consistent parsing for all platforms.
} | ||
|
||
if (Z_TYPE_P(increment) != IS_STRING) { | ||
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected increment to be integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(increment))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HHVM will throw "Expected timestamp to be an unsigned 32-bit integer or string" on type errors (see: here). I'm happy to submit a PR to revise that once this is approved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the HHVM warning is not wrong? It has to be an unsigned 32-bit integer. I don't see you test for this here, but it's in php_phongo_timestamp_init I believe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
php_phongo_timestamp_init()
performs range checking and will throw the "Expected increment to be an unsigned 32-bit integer, %d given" exceptions. I can change this to "Expected timestamp to be an unsigned 32-bit integer or string" and remove the "%s given" bit. Sharing the unexpected type was for consistency with our other exceptions for type errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that for HHVM, we should add the "%s given" bit, and you change the message to "Expected increment to be an unsigned 32-bit integer or string, %s given" — does that make sense? (note, in your reply you used timestamp and not increment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me. The timestamp/increment was indeed a typo on my part. I'll take care of the HHVM PR.
} | ||
|
||
zend_restore_error_handling(&error_handling TSRMLS_CC); | ||
if (Z_TYPE_P(milliseconds) != IS_STRING) { | ||
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(milliseconds))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HHVM only throws "Expected instance of DateTimeInterface" and "Error parsing %s as 64-bit integer" exceptions, so it may need a PR for this (see here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the HHVM message is much clearer as an API point of view. The "error parsing x as 64-bit integer" is what people are interested in. Whether it's an "integer", "float", or "string" is irrelevant. The current message in your code also misses "float".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I omitted "float" from the message since the cast to a long is just for the user's convenience. I wasn't planning on documenting that we accept a float, since it is being truncated. This is mainly to skirt around scalar type hint errors.
In HHVM's case, the invalid argument is cast to a string value for the exception message. That leads to some odd output for null (empty string), boolean ("1" and "0"), and would likely emit some notice on an array. The type error I was using here was consistent with what we already did for ReadPreference, WriteConcern, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK - add some tests for these additional types, so I can test HHVM against it as well? (If you haven't done so)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are in bson-utcdatetime_error-004.phpt
.
@@ -8,19 +8,19 @@ MongoDB\BSON\Timestamp::__set_state() with broken numeric strings | |||
require_once __DIR__ . '/../utils/tools.php'; | |||
|
|||
echo throws(function() { | |||
MongoDB\BSON\Timestamp::__set_state(['increment' => 'broken', 'timestamp' => '5678']); | |||
MongoDB\BSON\Timestamp::__set_state(['increment' => '1.23', 'timestamp' => '5678']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test would have been problematic for Windows since _atoi64()
stops parsing at the first invalid character without raising an error; however, bson_ascii_strtoll()
is more strict.
OK: Got MongoDB\Driver\Exception\InvalidArgumentException | ||
Expected increment to be integer or string, %r(null|NULL)%r given | ||
OK: Got MongoDB\Driver\Exception\InvalidArgumentException | ||
Expected increment to be integer or string, %r(double|float)%r given |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pattern is necessary for PHP 5 and 7.
<?php exit(0); ?> | ||
--EXPECTF-- | ||
OK: Got MongoDB\Driver\Exception\InvalidArgumentException | ||
Expected increment to be integer or string, %r(null|NULL)%r given |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pattern is necessary for PHP and HHVM.
@@ -1,26 +1,24 @@ | |||
--TEST-- | |||
MongoDB\BSON\Timestamp::__set_state() with broken numeric strings | |||
--SKIPIF-- | |||
<?php if (8 !== PHP_INT_SIZE) { die('skip Only for 64-bit platform'); } ?> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this was erroneously copy-pasted when the test was created in d25e565.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly fine, but some queries about error messages.
} | ||
|
||
if (Z_TYPE_P(increment) != IS_STRING) { | ||
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected increment to be integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(increment))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the HHVM warning is not wrong? It has to be an unsigned 32-bit integer. I don't see you test for this here, but it's in php_phongo_timestamp_init I believe.
zend_restore_error_handling(&error_handling TSRMLS_CC); | ||
return; | ||
} | ||
if (Z_TYPE_P(milliseconds) == IS_DOUBLE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could cause data loss, and I'm not keen about that :-/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I admittedly has some trouble reading through all of the zend_dval_to_lval()
cases to see if this would be an issue. Would you agree that the best solution is to convert this to a string via "%f.0"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't think that's needed. It's just that we ignore a user's sub-milliseconds, because they passed in a float, without notifying them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this isn't a matter of stuffing a double into a 32-bit integer, I don't understand the concern. UTCDateTime is documented as only representing milliseconds and the purpose of this change was because users previously had to use microtime(true) * 1000
to create a UTCDateTime with the current time. This is no different than a userland method using an (int)
cast to ensure it's working with an integer (before scalar type hinting was available).
That said, I think the 32-bit issue is still a concern worth addressing. microtime(true) * 1000
will easily result in values that exceed INT32_MAX
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree. That should be handled.
} | ||
|
||
zend_restore_error_handling(&error_handling TSRMLS_CC); | ||
if (Z_TYPE_P(milliseconds) != IS_STRING) { | ||
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(milliseconds))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the HHVM message is much clearer as an API point of view. The "error parsing x as 64-bit integer" is what people are interested in. Whether it's an "integer", "float", or "string" is irrelevant. The current message in your code also misses "float".
@@ -6,7 +6,7 @@ MongoDB\BSON\UTCDateTime unserialization requires "milliseconds" string to parse | |||
require_once __DIR__ . '/../utils/tools.php'; | |||
|
|||
echo throws(function() { | |||
unserialize('C:24:"MongoDB\BSON\UTCDateTime":40:{a:1:{s:12:"milliseconds";s:7:"INVALID";}}'); | |||
unserialize('C:24:"MongoDB\BSON\UTCDateTime":42:{a:1:{s:12:"milliseconds";s:9:"1234.5678";}}'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have kept the length of the argument the same, as to not have to mess with lengths in hardcoded serialized strings. But that's done now.
zend_restore_error_handling(&error_handling TSRMLS_CC); | ||
return; | ||
} | ||
if (Z_TYPE_P(milliseconds) == IS_DOUBLE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree. That should be handled.
Changing "s" to "z" in zend_parse_parameters() allows integers to be accepted in strict types mode. If all necessary parameters are integers, we can also skip conversion to a string and back to a 64-bit integer.
zend_restore_error_handling(&error_handling TSRMLS_CC); | ||
return; | ||
} | ||
tmp_len = snprintf(tmp, sizeof(tmp), "%.0f", Z_DVAL_P(milliseconds) > 0 ? floor(Z_DVAL_P(milliseconds)) : ceil(Z_DVAL_P(milliseconds))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trunc()
is available in C99, but I didn't want to risk any Windows build issues for older PHP issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it make more sense to convert to a long straight away here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally used convert_to_long()
, but you mentioned it would lose data. Although you were referring to precision after milliseconds, which I later explained we don't care about, that solution did have issues on 32-bit platforms. This method ensures that values larger than INT32_MAX in a float will convert properly on 32-bit.
https://jira.mongodb.org/browse/PHPC-790
https://jira.mongodb.org/browse/PHPC-804